Data Mining Cup 2014¶

Pengembalian barang merupakan faktor biaya yang sangat signifikan bagi retail online. Rata-rata setengah dari semua pesanan pelanggan adalah pengembalian barang. Topik ini akan menjadi semakin penting dengan diperkenalkannya petunjuk perlindungan konsumen. Oleh karena itu, tingkat pengembalian yang lebih rendah akan menjadi faktor utama dalam keunggulan kompetitif dalam ritel online.

Berdasarkan data pembelian historis dari sebuah toko online, sebuah model harus dipelajari untuk menghasilkan prediksi probabilitas bahwa pembelian tertentu dikonversi menjadi pengembalian berdasarkan data pembelian baru dari toko tersebut. Untuk tujuan ini, data historis juga berisi data pembelian dan pengiriman sebagai atribut produk dan pelanggan yang berbeda. Informasi "return yes/no" juga diketahui dari data historis. Data historis berisi data pembelian dan pengiriman toko online 2014.

  • orderItemID: Nomor barang pesanan
  • orderDate: Tanggal pemesanan
  • deliveryDate : Tanggal pesanan dikirim
  • itemID: ID barang
  • size: Ukuran barang
  • color: Warna barang
  • manufacturerID: ID manufaktur/pabrik
  • price: Harga barang
  • customerID: ID pelanggan
  • salutation: Salutation pelanggan
  • dateOfBirth: Tanggal lahir pelanggan
  • state: Negara pelanggan
  • creationDate: Tanggal pembuatan akun

Target: returnShipment | returnShipment: Pengembalian (1=ya/dikembalikan, 0=tidak/disimpan). Memprediksi apakah ada pengembalian barang pada pembelian berdasarkan data pembelian baru toko tersebut. Atribut target "returnShipment" dari item pesanan. Nilai "0" berarti "barang disimpan" dan nilai "1" berarti "barang dikembalikan".

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os, glob
In [2]:
#load dataset
df = pd.read_csv("E:\\data\\orders_train.txt", sep=';', na_values=["?"])
In [3]:
# Menampilkan dataframe
df.head()
Out[3]:
orderItemID orderDate deliveryDate itemID size color manufacturerID price customerID salutation dateOfBirth state creationDate returnShipment
0 1 2012-04-01 2012-04-03 186 m denim 25 69.90 794 Mrs 1965-01-06 Baden-Wuerttemberg 2011-04-25 0
1 2 2012-04-01 2012-04-03 71 9+ ocher 21 69.95 794 Mrs 1965-01-06 Baden-Wuerttemberg 2011-04-25 1
2 3 2012-04-01 2012-04-03 71 9+ curry 21 69.95 794 Mrs 1965-01-06 Baden-Wuerttemberg 2011-04-25 1
3 4 2012-04-02 NaN 22 m green 14 39.90 808 Mrs 1959-11-09 Saxony 2012-01-04 0
4 5 2012-04-02 1990-12-31 151 39 black 53 29.90 825 Mrs 1964-07-11 Rhineland-Palatinate 2011-02-16 0

Exploratory Data Analysis¶

In [4]:
# Melihat dimensi dataset (jumlah baris dan kolom)
df.shape
Out[4]:
(481092, 14)
In [5]:
# Informasi detail dataset
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 481092 entries, 0 to 481091
Data columns (total 14 columns):
 #   Column          Non-Null Count   Dtype  
---  ------          --------------   -----  
 0   orderItemID     481092 non-null  int64  
 1   orderDate       481092 non-null  object 
 2   deliveryDate    441673 non-null  object 
 3   itemID          481092 non-null  int64  
 4   size            481092 non-null  object 
 5   color           480949 non-null  object 
 6   manufacturerID  481092 non-null  int64  
 7   price           481092 non-null  float64
 8   customerID      481092 non-null  int64  
 9   salutation      481092 non-null  object 
 10  dateOfBirth     432203 non-null  object 
 11  state           481092 non-null  object 
 12  creationDate    481092 non-null  object 
 13  returnShipment  481092 non-null  int64  
dtypes: float64(1), int64(5), object(8)
memory usage: 51.4+ MB
In [6]:
# Menampilkan tipe data dari setiap kolom
df.dtypes
Out[6]:
orderItemID         int64
orderDate          object
deliveryDate       object
itemID              int64
size               object
color              object
manufacturerID      int64
price             float64
customerID          int64
salutation         object
dateOfBirth        object
state              object
creationDate       object
returnShipment      int64
dtype: object
In [7]:
# Mengecek jumlah data kosong
df.isnull().sum()
Out[7]:
orderItemID           0
orderDate             0
deliveryDate      39419
itemID                0
size                  0
color               143
manufacturerID        0
price                 0
customerID            0
salutation            0
dateOfBirth       48889
state                 0
creationDate          0
returnShipment        0
dtype: int64
In [8]:
# Visualisasi persentase data kosong
import plotly.graph_objects as go

# Hitung persentase data kosong per kolom
missing_percentage = df.isnull().mean() * 100

# Membuat bar chart menggunakan plotly
fig = go.Figure()

# Menambahkan data untuk bar chart
fig.add_trace(go.Bar(
    x=missing_percentage.index,
    y=missing_percentage,
    text=[f'{v:.2f}%' for v in missing_percentage],
    textposition='outside',
    marker=dict(color='skyblue'),
))

# Menambahkan judul dan label sumbu
fig.update_layout(
    title=dict(
        text="Persentase Data Kosong",
        x=0.5,
        y=0.9,
        font=dict(size=16)
    ),
    yaxis_title="Persentase Data Kosong (%)",
    template="plotly_white",
)

# Menampilkan plot
fig.show()

Dari hasil pengecekan data kosong, kolom salutation memiliki persentase data kosong tertinggi, yaitu 10.16%. Kolom deliveryDate memiliki persentase data kosong 8.19% dan kolom color 0.03% data kosong. Sebagian kolom lainnya memiliki persentase data kosong 0% yang menunjukkan bahwa data pada kolom-kolom ini relatif lengkap.

In [9]:
import plotly.express as px

# Melihat perngaruh data kosong terhadap data returnShipemnt
vis = df.copy()
df_na = [a for a in vis.columns if vis[a].isna().sum() > 0] # Menentukan kolom dengan data kosong

# Perulangan untuk setiap fitur yang memiliki nilai kosong
for a in df_na:
    # Membuat salinan data dan mengganti NaN dengan no dan yang lainnya menjadi yes
    data_na = vis.copy()
    data_na[a] = np.where(data_na[a].isna(), 'no', 'yes')
    
    # Membuat plot menggunakan Plotly Express
    fig = px.histogram(
        data_na, 
        x='returnShipment', 
        color=a,
        barmode='group',
        title=f'Distribusi Data Kosong pada Fitur {a}',
        labels={'returnShipment': 'Return Shipment'},
        category_orders={'returnShipment': ['yes', 'no']}, 
        color_discrete_map={'no': 'indianred', 'yes': 'cornflowerblue'}
    )
    
    fig.update_layout(
        title_x=0.5,
        legend_title=a
    )
    
    # Menampilkan grafik
    fig.show()
  • Pada fitur deliveryDate, jumlah data yang tidak kosong jauh lebih banyak daripada data yang kosong, baik untuk barang yang dikembalikan maupun yang tidak. Pada kategori barang yang tidak dikembalikan (returnShipment = 0) terdapat data kosong sebanyak 39.419 yang menunjukkan bahwa banyak nilai kosong berhubungan dengan kategori returnShipment = 0. Dapat dilihat data kosong pada deliveryDate sebanyak 39.419 semuanya dari kategori barang yang tidak dikembalikan (returnShipment = 0).
  • Pada fitur color, untuk jumlah data yang tidak kosong juga jauh lebih banyak daripada data yang kosong, baik untuk barang yang dikembalikan maupun yang tidak.
  • Sama seperti dua fitur sebelumnya, pada fitur dateOfBirth, jumlah data yang tidak kosong jauh lebih banyak daripada data yang kosong, baik untuk barang yang dikembalikan maupun yang tidak. Pada masing-masing kategori returnShipment terdapat nilai kosong, dari total nilai kosong pada fitur ini yaitu 48.889, sebanyak 25.599 nilai kosong pada kategori returnShipment = 0 (barang tidak dikembalikan) dan sebanyak 23.290 nilai kosong pada kategori returnShipment = 1 (barang dikembalikan).
In [10]:
# Melakukan Uji Chi-Square untuk melihat apakah ada hubungan antara keberadaan data kosong 
# pada fitur-fitur yang memiliki data kosong dengan kemungkinan pengembalian barang (returnShipment)
from scipy.stats import chi2_contingency

# Menyimpan hasil uji Chi-Square
chi_square_results = {}

# Loop untuk setiap kolom dengan data kosong
for a in df_na:
    # Mengganti nilai kosong (NaN) menjadi 1 (kosong), dan nilai lainnya menjadi 0 (tidak kosong)
    data_na = vis.copy()
    data_na[a] = data_na[a].isna().astype(int)
    
    # Membuat tabel kontingensi
    contingency_table = pd.crosstab(data_na['returnShipment'], data_na[a])
    
    # Melakukan uji Chi-Square
    chi2, p, dof, expected = chi2_contingency(contingency_table)
    
    # Menyimpan hasil uji Chi-Square
    chi_square_results[a] = {
        'chi2_statistic': chi2,
        'p_value': p,
        'degrees_of_freedom': dof
    }

# Menampilkan hasil
for feature, result in chi_square_results.items():
    print(f"Uji Chi-Square untuk {feature}:")
    print(f"Chi-Square Statistic: {result['chi2_statistic']}")
    print(f"P-Value: {result['p_value']}")
    print(f"Degree of Freedom: {result['degrees_of_freedom']}")
    if p < 0.05:
        print("Terdapat hubungan signifikan antara nilai kosong dan kategori returnShipment.\n")
    else:
        print("Tidak terdapat hubungan signifikan antara nilai kosong dan kategori returnShipment.\n")
Uji Chi-Square untuk deliveryDate:
Chi-Square Statistic: 40019.094469339616
P-Value: 0.0
Degree of Freedom: 1
Terdapat hubungan signifikan antara nilai kosong dan kategori returnShipment.

Uji Chi-Square untuk color:
Chi-Square Statistic: 105.91419902455746
P-Value: 7.699792024944289e-25
Degree of Freedom: 1
Terdapat hubungan signifikan antara nilai kosong dan kategori returnShipment.

Uji Chi-Square untuk dateOfBirth:
Chi-Square Statistic: 7.924416076770826
P-Value: 0.0048772082880166965
Degree of Freedom: 1
Terdapat hubungan signifikan antara nilai kosong dan kategori returnShipment.

Tingkat signifikansi (P-Value) yang digunakan adalah 0.05.

  • Ketiga uji Chi-Square menunjukkan bahwa terdapat hubungan yang signifikan antara keberadaan data kosong pada fitur deliveryDate, color, dan dateOfBirth dengan kemungkinan pengembalian barang (returnShipment). Nilai P-Value pada ketiga fitur tersebut sangat kecil di bawah 0 jauh di bawah tingkat signifikansi (0.05). Ini berarti menolak hipotesis nol, yang menyatakan bahwa tidak ada hubungan antara keberadaan data kosong pada ketiga fitur tersebut dan returnShipment.
  • Dengan kata lain, terdapat hubungan yang sangat signifikan antara nilai kosong pada ketiga fitur tersebut dan kategori returnShipment. Artinya, keberadaan data kosong pada fitur deliveryDate, color, dan dateOfBirth berhubungan dengan kemungkinan pengembalian barang (returnShipment).
Menangani Data Kosong¶
In [11]:
# Salin data untuk proses preproses
df2 = df.copy()
In [12]:
# Mengisinya dengan color yang paling sering muncul (mode)
df2.color.fillna(df2.color.mode()[0], inplace=True)
In [13]:
df2.isnull().sum()
Out[13]:
orderItemID           0
orderDate             0
deliveryDate      39419
itemID                0
size                  0
color                 0
manufacturerID        0
price                 0
customerID            0
salutation            0
dateOfBirth       48889
state                 0
creationDate          0
returnShipment        0
dtype: int64
In [14]:
# Cek nilai unik color
df2.color.unique()
Out[14]:
array(['denim', 'ocher', 'curry', 'green', 'black', 'brown', 'red',
       'mocca', 'anthracite', 'olive', 'petrol', 'blue', 'grey', 'beige',
       'ecru', 'turquoise', 'magenta', 'purple', 'pink', 'khaki', 'navy',
       'habana', 'silver', 'white', 'nature', 'stained', 'orange',
       'azure', 'apricot', 'mango', 'berry', 'ash', 'hibiscus', 'fuchsia',
       'blau', 'dark denim', 'mint', 'ivory', 'yellow', 'bordeaux',
       'pallid', 'ancient', 'baltic blue', 'almond', 'aquamarine',
       'brwon', 'aubergine', 'aqua', 'dark garnet', 'dark grey',
       'avocado', 'creme', 'champagner', 'cortina mocca',
       'currant purple', 'cognac', 'aviator', 'gold', 'ebony',
       'cobalt blue', 'kanel', 'curled', 'caramel', 'antique pink',
       'darkblue', 'copper coin', 'terracotta', 'basalt', 'amethyst',
       'coral', 'jade', 'opal', 'striped', 'mahagoni', 'floral',
       'dark navy', 'dark oliv', 'vanille', 'ingwer', 'iron', 'graphite',
       'leopard', 'oliv', 'bronze', 'crimson', 'lemon', 'perlmutt'],
      dtype=object)

Nilai unik color terlalu banyak, karena itu perlu dilakukan grouping kategori yang jarang muncul. Untuk color yang kemunculannya kurang dari 5000 kali akan dikategorikan ke dalam 'other'

In [15]:
# Hitung frekuensi masing-masing color
color_counts = df2['color'].value_counts()

# Tentukan threshold untuk kategori yang jarang muncul
threshold = 3500  # Kemunculan kurang dari 3500 kali

# Ganti color yang jarang muncul dengan 'other'
df2['color'] = df2['color'].apply(lambda x: x if color_counts[x] >= threshold else 'other')
In [16]:
# Cek nilai unik color
df2.color.unique()
Out[16]:
array(['denim', 'ocher', 'other', 'green', 'black', 'brown', 'red',
       'mocca', 'anthracite', 'olive', 'petrol', 'blue', 'grey', 'ecru',
       'turquoise', 'purple', 'pink', 'white', 'stained', 'orange',
       'berry', 'ash', 'aquamarine', 'aubergine'], dtype=object)
In [17]:
# Jumlah masing-masing warna
df2['color'].value_counts().head()
Out[17]:
black    86395
blue     48180
grey     42273
other    41916
red      39074
Name: color, dtype: int64

Setelah dikategorikan dengan menentukan threshold, untuk kategori color yang banyak muncul yaitu black.

Pada atribut deliveryDate dan dateOfBirth karena tipe data 2 atribut tersebut object, maka sebelum penanganan data kosong melakukan konversi tipe data terlebih dahulu dari tipe object ke tipe datetime.

In [18]:
# Mengubah tipe data deliveryDate dan dateOfBirth dari tipe data objek ke tipe datatime
# Mambahkan parameter errors untuk mengabaikan out-of-bound

# Konversi data deliveryDate dari objek menjadi datetime
df2["deliveryDate"] = pd.to_datetime(df2["deliveryDate"], format='%Y-%m-%d', errors = 'coerce')

# Konversi data dateOfBirth dari objek menjadi datetime
df2["dateOfBirth"] = pd.to_datetime(df2["dateOfBirth"], format='%Y-%m-%d', errors = 'coerce')
In [19]:
# Cek tipe data
df2.dtypes
Out[19]:
orderItemID                int64
orderDate                 object
deliveryDate      datetime64[ns]
itemID                     int64
size                      object
color                     object
manufacturerID             int64
price                    float64
customerID                 int64
salutation                object
dateOfBirth       datetime64[ns]
state                     object
creationDate              object
returnShipment             int64
dtype: object
In [20]:
# Melihat nilai unik dari DeliveryDate
unique_years = df2['deliveryDate'].dt.year.unique()
print(unique_years)
[2012.   nan 1990. 2013.]
In [21]:
# Melihat data orderDate dan deliveryDate
df2[['orderDate', 'deliveryDate']].head(10)
Out[21]:
orderDate deliveryDate
0 2012-04-01 2012-04-03
1 2012-04-01 2012-04-03
2 2012-04-01 2012-04-03
3 2012-04-02 NaT
4 2012-04-02 1990-12-31
5 2012-04-02 1990-12-31
6 2012-04-02 1990-12-31
7 2012-04-02 2012-04-03
8 2012-04-02 2012-04-03
9 2012-04-02 2012-04-03

Untuk penanganan data kosong pada fitur deliveryDate terdapat beberapa pengecekan dan proses lainnya.

  • Dari pengecekan nilai unik fitur deliveryDate terdapat tahun yang sangat jauh dari tahun 2012-2013 yaitu tahun 1990.
  • Dapat dilihat dari fitur orderDate dan deliveryDate menunjukkan terdapat data orderDate (pemesanan barang) di tahun 2012 tetapi pengirimannya (deliveryDate) pada tahun 1990.

Dari penemuan tersebut, sebelum menangani data kosong fitur deliveryDate, dilakukan penyesuaian terlebih dahulu dengan mengganti tahun dan bulan pada deliveryDate yang memiliki tahun 1990 disesuaikan dengan tahun dan bulan pada orderDate. Sedangkan untuk tanggalnya diisi random 3-5 hari dari tanggal pemesanan (orderDate).

In [22]:
import random
# Mengubah kolom 'orderDate' menjadi tipe datetime
df2['orderDate'] = pd.to_datetime(df2['orderDate'], errors='coerce')

# Fungsi untuk mengganti tahun, bulan, dan hari deliveryDate mengacu pada orderDate
def adjust_delivery_date(row):
    # Memeriksa apakah tahun deliveryDate adalah 1990
    if row['deliveryDate'].year == 1990:
        # Ambil bulan dan tahun dari orderDate
        year = row['orderDate'].year
        month = row['orderDate'].month
        
        # Ambil + 3-5 hari dari orderDate 
        day = row['orderDate'].day + random.choice([3, 5])
        
        # Menghindari kesalahan jika hari yang ditambahkan melebihi batas bulan
        try:
            adjusted_date = row['deliveryDate'].replace(year=year, month=month, day=day)
        except ValueError:
            # Jika hari yang ditambahkan melebihi batas, set ke hari terakhir bulan tersebut
            adjusted_date = row['deliveryDate'].replace(year=year, month=month, day=1) + pd.Timedelta(days=-1)
        
        return adjusted_date
    return row['deliveryDate']

# Terapkan fungsi mengganti tahun, bulan, dan hari
df2['deliveryDate'] = df2.apply(adjust_delivery_date, axis=1)
In [23]:
# Melihat data orderDate dan deliveryDate setelah penyesuaian tahun deliveryDate
df2[['orderDate', 'deliveryDate']].head(10)
Out[23]:
orderDate deliveryDate
0 2012-04-01 2012-04-03
1 2012-04-01 2012-04-03
2 2012-04-01 2012-04-03
3 2012-04-02 NaT
4 2012-04-02 2012-04-05
5 2012-04-02 2012-04-07
6 2012-04-02 2012-04-07
7 2012-04-02 2012-04-03
8 2012-04-02 2012-04-03
9 2012-04-02 2012-04-03
In [24]:
# Melihat nilai unik dari DeliveryDate setelah penyesuaian tahun
unique_years = df2['deliveryDate'].dt.year.unique()
print(unique_years)
[2012.   nan 2013.]

Untuk mengisi nilai kosong pada deliveryDate menggunakan jarak waktu relatif dari orderDate, dengan menambahkan 2-5 hari setelah orderDate.

In [25]:
# Cek data kosong
df2.isnull().sum()
Out[25]:
orderItemID           0
orderDate             0
deliveryDate      39419
itemID                0
size                  0
color                 0
manufacturerID        0
price                 0
customerID            0
salutation            0
dateOfBirth       48892
state                 0
creationDate          0
returnShipment        0
dtype: int64
In [26]:
# Fungsi untuk mengisi deliveryDate berdasarkan orderDate
def fill_delivery_date(row):
    if pd.isna(row['deliveryDate']):  # Jika deliveryDate kosong
        # Tambahkan antara 2 hingga 5 hari ke orderDate
        penambahan_hari = random.randint(2, 5)
        return row['orderDate'] + pd.Timedelta(days=penambahan_hari)
    return row['deliveryDate']  # Jika tidak kosong, biarkan deliveryDate tetap

# Terapkan fungsi ke dataframe
df2['deliveryDate'] = df2.apply(fill_delivery_date, axis=1)
In [27]:
# Cek data kosong
df2.isnull().sum()
Out[27]:
orderItemID           0
orderDate             0
deliveryDate          0
itemID                0
size                  0
color                 0
manufacturerID        0
price                 0
customerID            0
salutation            0
dateOfBirth       48892
state                 0
creationDate          0
returnShipment        0
dtype: int64

Untuk menangani data kosong fitur dateOfBirth dilakukan beberapa proses lain seperti cek tahun maksimum dan minimum dan cek nilai unik fitur dateOfBirth untuk melihat rentang tahun. Kemudian lihat usia per tahun 2014, untuk memfilter usia yang tidak relevan seperti terlalu muda (15 tahun ke bawah) atau terlalu tua (80 tahun ke atas).

In [28]:
# Melihat nilai maksimal dan minimal dateOfBirth
print("Tanggal Maks dateOfBirth :", df2.dateOfBirth.max())
print("Tanggal Min dateOfBirth :", df2.dateOfBirth.min())
Tanggal Maks dateOfBirth : 2013-06-27 00:00:00
Tanggal Min dateOfBirth : 1900-11-19 00:00:00
In [29]:
# Melihat nilai unik dari dateOfBirth
df2['dateOfBirth'].dt.year.unique()
Out[29]:
array([1965., 1959., 1964., 1948.,   nan, 1963., 1953., 1961., 1978.,
       1968., 1940., 1966., 1945., 1943., 1958., 1955., 1960., 1900.,
       1956., 1977., 1954., 1969., 1971., 1973., 1962., 1957., 1970.,
       1950., 1967., 1951., 1974., 1972., 1949., 1952., 1980., 1976.,
       1927., 1938., 1982., 1990., 1946., 1901., 1981., 1944., 1985.,
       1986., 1983., 1979., 1975., 1947., 1988., 1984., 1942., 1987.,
       1991., 1929., 1992., 1925., 1993., 1937., 1941., 1939., 2010.,
       1931., 1908., 1936., 1934., 1920., 1933., 1989., 2011., 1999.,
       1932., 1935., 1921., 1928., 1926., 2009., 1907., 1998., 2000.,
       1996., 1911., 2005., 1906., 1995., 1903., 1994., 1930., 1912.,
       2012., 1918., 1997., 1924., 1917., 2013.])
In [30]:
# Cek usia
cek_usia = df2.copy()
cek_usia['age'] = 2014 - cek_usia['dateOfBirth'].dt.year
In [31]:
# Menampilkan usia di atas 80 tahun atau di bawah 17 tahun
cek_usia[(cek_usia['age'] > 80) | (cek_usia['age'] < 17)][['dateOfBirth', 'age']].head(10)
Out[31]:
dateOfBirth age
79 1900-11-19 114.0
374 1927-04-10 87.0
570 1900-11-19 114.0
571 1900-11-19 114.0
572 1900-11-19 114.0
573 1900-11-19 114.0
574 1900-11-19 114.0
575 1900-11-19 114.0
653 1901-09-25 113.0
654 1901-09-25 113.0
  • Dari pengecekan nilai maksimal dan minimal dateOfBirth dan pengecekan nilai unik dari fitur dateOfBirth, jika mengacu ke tanggal pemesanan (orderDate) yang ada di kisaran tahun 2012-2013, terdapat usia yang tidak relevan.
  • Setelah pengecekan usia, data tahun lahir seperti tahun 2000-2013 yang memiliki kisaran usia 1-14 tahun dan tahun lahir 1900 usianya sudah di atas 100 tahun, seharusnya tidak melakukan pemesanan. Jadi, sebelum menangani data kosong dateOfBirth, menyesuaikan tahun lahir terlebih dahulu.

Di sini akan mengambil data tahun lahir valid di antara tahun 1935 sampai 1997 atau di kisaran usia 17-80 tahun. Untuk tahun lahir di luar ketentuan tersebut akan diganti, dan karena tahunnya bervariasi untuk mengganti tahunnya menggunakan median.

In [32]:
# Hitung median tahun yang valid (1935-1997)
tahun_valid = df2[df2['dateOfBirth'].dt.year.between(1935, 1997)]['dateOfBirth'].dt.year
median_tahun = tahun_valid.median()

# Ganti tahun yang tidak valid (di bawah 1935 atau di atas 1997) dengan median
df2['dateOfBirth'] = df2['dateOfBirth'].apply(
    lambda x: pd.Timestamp(f"{int(median_tahun)}-{x.month:02d}-{x.day:02d}")
    if x.year < 1935 or x.year > 1997 else x
)
In [33]:
# Melihat nilai maksimal dan minimal dateOfBirth setelah mengubah dengan median
print("Tanggal Maks dateOfBirth :", df2.dateOfBirth.max())
print("Tanggal Min dateOfBirth :", df2.dateOfBirth.min())
Tanggal Maks dateOfBirth : 1997-09-06 00:00:00
Tanggal Min dateOfBirth : 1935-01-04 00:00:00

Sama seperti halnya mengganti tahun yang tidak valid, untuk menangani data kosong karena tahunnya bervariasi untuk mengganti tahunnya menggunakan median.

In [34]:
# Fungsi untuk mengganti tahun yang hilang dengan median
def replace_with_median(row):
    if pd.isna(row):  # Jika data kosong (NaN) atau NaT
        # Jika data kosong, ganti tahun dengan median
        return pd.Timestamp(f"{int(median_tahun)}-01-01")
    # Jika data tidak kosong, kembalikan baris tanpa perubahan
    return row

# Terapkan fungsi ke dataframe
df2['dateOfBirth'] = df2['dateOfBirth'].apply(replace_with_median)
In [35]:
# Cek data kosong
df2.isnull().sum()
Out[35]:
orderItemID       0
orderDate         0
deliveryDate      0
itemID            0
size              0
color             0
manufacturerID    0
price             0
customerID        0
salutation        0
dateOfBirth       0
state             0
creationDate      0
returnShipment    0
dtype: int64
Mengubah fitur size menjadi konsisten¶
In [36]:
# Melihat nilai unik dari size 
df2['size'].unique()
Out[36]:
array(['m', '9+', '39', 'xxl', '37', '43', '38', 'l', 'xl', '42', '41',
       'unsized', 's', '10+', '40', '36', '152', '35', '34', '8+', '9',
       '46', '6', '10', '25', '20', '5', '42+', '44', '4+', '8', '3',
       '6+', '48', '7+', '50', '22', '12', '45', '7', '24', '36+', '39+',
       '27', '32', '11', '26', '40+', '19', '21', '5+', '116', '2', '28',
       '38+', '11+', '37+', '164', '4', '33', '29', '30', '18', '41+',
       '1', '47', '31', '104', '128', '95', '3+', '140', '23', '13',
       '3332', 'S', '44+', 'xxxl', '54', '52', '3432', '43+', '3434',
       '49', '84', '56', '14', '13+', '76', '90', '85', '176', '88',
       '45+', 'L', '46+', '80', '3632', '3832', '3634', '4032', 'xs',
       '2+', '100', '3132', '58', '4034', '105', '3834', '12+', '2932',
       'M', '110', '122', 'XXL', 'XL', 'XXXL', '4232', 'XS', '92', '96',
       '3334'], dtype=object)

Dari pengecekan nilai unik dari fitur size, terdapat nilai dengan ukuran yang memiliki huruf kapital menjadi huruf kecil, seperti ukuran 'm' dan 'M'. Selain itu, terdapat tanda '+' yang mengarah pada perbedaan yang tidak perlu. Untuk itu, ukuran akan diubah menjadi kapital semua dan menghapus tanda '+'. Kemudian mengategorikan ukuran numerik menjadi ukuran dalam bentuk huruf.

In [37]:
# Normalisasi dan buang tanda '+'
df2['size'] = df2['size'].str.upper()  # Ubah ke kapital semua
df2['size'] = df2['size'].str.replace(r'\+', '', regex=True)  # Hapus tanda '+'

# Mengelompokkan ukuran berdasarkan angka
def categorize_size(size):
    # Kategori ukuran standar
    size_categories = ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL']
    if size in size_categories: 
        return size
    try:
        size_num = int(size)
        
        # Kategorisasi berdasarkan ukuran angka
        if size_num <= 10:
            return 'XS'
        elif 11 <= size_num <= 35:
            return 'S'
        elif 36 <= size_num <= 42:
            return 'M'
        elif 43 <= size_num <= 50:
            return 'L'
        elif 51 <= size_num <= 200:
            return 'XL'
        elif 201 <= size_num <= 1000:
            return 'XXL'
        elif size_num > 1000:
            return 'XXXL'
        
    except ValueError:
        return 'UNSIZED'


# Terapkan fungsi ke dataframe untuk mengkategorikan ukuran
df2['size'] = df2['size'].apply(categorize_size)
In [38]:
# Cek hasil fitur size
df2['size'].unique()
Out[38]:
array(['M', 'XS', 'XXL', 'L', 'XL', 'UNSIZED', 'S', 'XXXL'], dtype=object)
In [39]:
# Distribusi data kategorikal Size

# Menghitung frekuensi kategori
size_counts = df2['size'].value_counts().reset_index()
size_counts.columns = ['Kategori', 'Jumlah']

# Membuat bar chart
fig = px.bar(size_counts, x='Jumlah', y='Kategori', orientation='h', 
             title='Frekuensi Fitur Size', labels={'Jumlah': 'Jumlah', 'Kategori': 'Size'},
             color='Jumlah', color_continuous_scale='Earth')

fig.update_layout(
    title_x=0.5,
)

# Menampilkan plot
fig.show()
Mengonversi data yang bertipe objek¶
In [40]:
# Cek fitur dengan tipe data objek
df2.select_dtypes(include = ['object']).head()
Out[40]:
size color salutation state creationDate
0 M denim Mrs Baden-Wuerttemberg 2011-04-25
1 XS ocher Mrs Baden-Wuerttemberg 2011-04-25
2 XS other Mrs Baden-Wuerttemberg 2011-04-25
3 M green Mrs Saxony 2012-01-04
4 M black Mrs Rhineland-Palatinate 2011-02-16
In [41]:
# Mengubah data tipe objek menjadi kategori
kategori = ['size', 'color', 'salutation', 'state']

# Looping untuk merubah tipe data'
for column in kategori:
    df2[column] = df2[column].astype('category')
In [42]:
# Mengubah type data objek dengan format tanggal menjadi datetime
# Konversi data atribut creationDate menjadi datetime
df2["creationDate"] = df2["creationDate"].astype("datetime64[ns]")
In [43]:
# Cek tipe data
df2.dtypes
Out[43]:
orderItemID                int64
orderDate         datetime64[ns]
deliveryDate      datetime64[ns]
itemID                     int64
size                    category
color                   category
manufacturerID             int64
price                    float64
customerID                 int64
salutation              category
dateOfBirth       datetime64[ns]
state                   category
creationDate      datetime64[ns]
returnShipment             int64
dtype: object
Menangani Outlier¶
In [44]:
# Distribusi nilai untuk kolom dengan fitur numerik
df2[['price']].describe()
Out[44]:
price
count 481092.000000
mean 70.440229
std 45.502854
min 0.000000
25% 34.900000
50% 59.900000
75% 89.900000
max 999.000000
In [45]:
# Visualisasi distribusi fitur price
import plotly.express as px

# Membuat histogram
fig = px.histogram(df2, x='price', nbins=30, title='Distribusi Fitur Price')

# Menambahkan label untuk sumbu x dan y
fig.update_layout(
    xaxis_title="Price",
    yaxis_title="Frekuensi",
    title_x=0.5
)

# Menampilkan plot
fig.show()

Karena data numerik selain price merupakan data ID, maka untuk melihat distribusi fitur numerik hanya data price.

Dari visualisasi histogram distribusi harga, sebagian besar data terpusat di sisi kiri histogram, yaitu pada rentang harga yang lebih rendah. Ekor histogram memanjang ke kanan, menunjukkan bahwa ada beberapa data dengan harga yang jauh lebih tinggi. Batang tertinggi menunjukkan rentang harga yang paling sering muncul. Dalam gambar, rentang harga antara 0 dan sekitar 25-75 adalah yang paling umum. Ada kemungkinan adanya outlier pada rentang harga yang lebih tinggi mendekati 1000.

In [46]:
# Visualisasi outlier menggunakan Box Plot
fig = px.box(df2, y="price", title="Deteksi Outlier Fitur Price")
fig.update_layout(
    title_x=0.5
)
fig.show()
  • Dari visualisasi outlier fitur price tersebut, kotak (IQR) terletak di bagian bawah grafik menunjukkan bahwa sebagian besar nilai harga (price) berada di antara 0 dan sekitar 200.
  • Distribusi data denderung positif (Right-Skewed), median terletak lebih dekat ke Q1 daripada Q3 (berada di bagian bawah kotak).
  • Terdapat beberapa titik di atas whisker atas. Ada beberapa outlier signifikan di atas nilai 400, termasuk satu outlier ekstrem 999.
In [47]:
# Visualisasi outlier fitur price dengan returnShipment
fig = px.box(df2, x="returnShipment", y="price")
fig.show()
  • Terlihat bahwa median harga untuk barang yang dikembalikan (returnShipment = 1) sedikit lebih tinggi (median = 69.9) dibandingkan dengan barang yang tidak dikembalikan (returnShipment = 0) (median = 59.9).
  • Kotak untuk returnShipment = 1 sedikit lebih tinggi, menunjukkan bahwa variasi harga untuk barang yang dikembalikan sedikit lebih besar dibandingkan dengan barang yang tidak dikembalikan.
  • Secara keseluruhan, rentang harga untuk kedua kelompok (dengan dan tanpa pengembalian) relatif mirip, meskipun terdapat beberapa outliers pada kedua kelompok.
  • Terdapat beberapa outliers di kedua kelompok. Outliers ini lebih banyak terlihat pada kelompok barang yang tidak dikembalikan dengan beberapa outlier signifikan di atas nilai 400. Outlier ekstrim dengan nilai 999 ada di kedua barang yang dikembalikan maupun yang tidak dikembalikan.
In [48]:
# Menghitung IQR
Q1 = df2['price'].quantile(0.25)
Q3 = df2['price'].quantile(0.75)
IQR = Q3 - Q1

# Batas untuk mendeteksi outlier
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# Deteksi outlier
outliers = df2[(df2['price'] < lower_bound) | (df2['price'] > upper_bound)]
print(f"Jumlah outlier: {len(outliers)}")
Jumlah outlier: 14017
In [49]:
# Mengatasi outlier, mengganti dengan batas maksimum
df2['price'] = np.where(df2['price'] > upper_bound, upper_bound, df2['price'])
In [50]:
# Distribusi fitur price
df2[['price']].describe()
Out[50]:
price
count 481092.000000
mean 69.267591
std 41.627030
min 0.000000
25% 34.900000
50% 59.900000
75% 89.900000
max 172.400000
In [51]:
# Visualisasi outlier setelah menghapus outlier
fig = px.box(df2, y="price")
fig.show()
In [52]:
# Visualisasi distribusi fitur price
import plotly.express as px

# Membuat histogram
fig = px.histogram(df2, x='price', nbins=30, title='Distribusi Price')

# Menambahkan label untuk sumbu x dan y
fig.update_layout(
    xaxis_title="Price",
    yaxis_title="Frekuensi",
    title_x=0.5
)

# Menampilkan plot
fig.show()

Dari visualisasi box plot dan histogarm setalah penghapusan outlier sudah tidak ada lagi outlier. Distribusi data dilihat pada kotak boxplot dan histogram menunjukkan bahwa sebagian besar data harga terkonsentrasi di antara nilai sekitar 35 hingga 90. Visualisasi ini memberikan gambaran yang lebih representatif tentang distribusi harga "normal" dengan konsentrasi data yang lebih jelas di rentang 35-90.

In [53]:
# Visualisasi outlier fitur price dengan returnShipment setelah penghapusan outlier
fig = px.box(df2, x="returnShipment", y="price")
fig.show()

Terlihat untuk kedua returnShipment sudah tidak ada lagi outlier.

Visualisasi ini menunjukkan bahwa Rentang harga untuk kedua returnShipment terlihat mirip. Ini menunjukkan bahwa setelah penanganan outlier, rentang harga keseluruhan untuk kedua returnShipment tidak jauh berbeda. Selain itu ada sedikit perbedaan pada median harga antara barang yang dikembalikan (69.9) dan yang tidak dikembalikan (59.9), dengan barang yang dikembalikan cenderung sedikit lebih mahal. Namun, variabilitas (IQR) dan rentang harga keseluruhan hampir sama.

Distribusi Fitur Target¶
In [54]:
#Cek jumlah data pada returnShipment untuk melihat apakah data target "returShipment" imbalance atau tidak
shipment = len(df2.returnShipment)
disimpan_count = len(df2[df2.returnShipment == 0])
dikembalikan_count = len(df2[df2.returnShipment == 1])
disimpan_percentage = round(disimpan_count/shipment*100, 2)
dikembalikan_percentage = round(dikembalikan_count/shipment*100, 2)

print('Total shipment {}'.format(shipment))
print('Barang tidak dikembalikan {}'.format(disimpan_count))
print('Persentase tidak dikembalikan {}%'.format(disimpan_percentage))
print('Barang dikembalikan {}'.format(dikembalikan_count))
print('Persentase dikembalikan {}%'.format(dikembalikan_percentage))
Total shipment 481092
Barang tidak dikembalikan 249001
Persentase tidak dikembalikan 51.76%
Barang dikembalikan 232091
Persentase dikembalikan 48.24%
In [55]:
plt.figure(figsize=(6, 6))
ax = sns.countplot(x='returnShipment', data = df2)
plt.title('Return Shipment', fontsize=12)

for p in ax.patches:
        ax.annotate(format(p.get_height(), 'd'), (p.get_x() + p.get_width() / 2., p.get_height()),
                    ha = 'center', va = 'center', xytext = (0, 4), textcoords = 'offset points', fontsize=8)
In [56]:
plt.figure(figsize=(6, 6))
total = float(len(df2))
ax = sns.countplot(x='returnShipment', data=df2)
plt.title('Persentase Return Shipment', fontsize=12)

for p in ax.patches:
        ax.annotate(format(100 * p.get_height()/total, '.2f') + '%', (p.get_x() + p.get_width() / 2., p.get_height()),
                    ha = 'center', va = 'center', xytext = (0, 4), textcoords = 'offset points', fontsize=8)

Dari kedua visualisasi terhadap fitur target "returnShipment" di atas, terlihat bahwa jumlah observasi untuk kategori 0 (barang tidak dikembalikan) jauh lebih besar daripada jumlah observasi untuk kategori 1 (barang dikembalikan). Tepatnya, ada 249.001 atau 51.7% barang yang tidak dikembalikan dan 232.091 atau 48.2% barang yang dikembalikan.

Meskipun terdapat sedikit ketidakseimbangan kelas, perbandingan jumlah antara kedua kelas tidak terlalu ekstrem. Perbedaan antara 51.7% dan 48.2% tidak terlalu besar, jadi meskipun ada imbalance, dampaknya mungkin tidak terlalu signifikan dibandingkan jika perbedaannya sangat drastis.

In [57]:
# Distribusi data kategorikal Salutation

# Menghitung frekuensi kategori
salutation_counts = df2['salutation'].value_counts().reset_index()
salutation_counts.columns = ['Kategori', 'Jumlah']

# Membuat bar chart horizontal
fig = px.bar(salutation_counts, x='Jumlah', y='Kategori', orientation='h', 
             title='Frekuensi Salutation', labels={'Jumlah': 'Jumlah', 'Kategori': 'Salutation'},
             color='Jumlah', color_continuous_scale='Earth')

fig.update_traces(texttemplate='%{x:d}', textposition='outside')
fig.update_layout(
    title_x=0.5
)

# Menampilkan plot
fig.show()
In [58]:
# Distribusi data kategorikal State
# Menghitung frekuensi kategori
salutation_percentage = df2['salutation'].value_counts().reset_index()

# Menghitung persentase untuk setiap kategori
salutation_percentage.columns = ['Kategori', 'Jumlah']
salutation_percentage['Persentase'] = (salutation_percentage['Jumlah'] / salutation_percentage['Jumlah'].sum()) * 100

# Membuat bar chart horizontal
fig = px.bar(salutation_percentage, x='Persentase', y='Kategori', orientation='h', 
             title='Persentase Salutation', labels={'Persentase': 'Persentase', 'Kategori': 'Salutation'},
             color='Persentase', color_continuous_scale='Earth')

fig.update_traces(texttemplate='%{x:.2f}%', textposition='outside')

fig.update_layout(
    showlegend=False,
    title_x=0.5
)

# Menampilkan plot
fig.show()

Visualisasi ini secara jelas menunjukkan dominasi salutation "Mrs" dalam data mencapai 96%. Meskipun "Mr" juga memiliki jumlah yang signifikan mencapai 3.5%, kategori lainnya jauh lebih sedikit. Data ini menunjukkan adanya preferensi target audiens atau populasi yang didata memang didominasi oleh perempuan yang sudah menikah.

In [59]:
# Melihat banyaknya pesanan tiap salutation yang mengembalikan pesanan dengan yang tidak mengembalikan pesanan
df2.groupby(["returnShipment","salutation"])["salutation"].count()
Out[59]:
returnShipment  salutation  
0               Company            193
                Family            1061
                Mr                9864
                Mrs             237644
                not reported       239
1               Company            168
                Family             830
                Mr                6856
                Mrs             224125
                not reported       112
Name: salutation, dtype: int64
In [60]:
#visualisasi hubungan salutation dengan returnShipment
plt.figure(figsize=(8, 6))
total = float(len(df2['salutation']))
ax = sns.countplot(x="salutation", hue = "returnShipment", data=df2)

for p in ax.patches:
        ax.annotate(format(100 * p.get_height()/total, '.2f') + '%', (p.get_x() + p.get_width() / 2., p.get_height()),
                    ha = 'center', va = 'center', xytext = (0, 6), textcoords = 'offset points', fontsize=8)

Dari semua data dengan sapaan "Mrs", 49.40% barang tidak dikembalikan dan 46.59% dikembalikan, perbedaan ini relatif kecil, menunjukkan bahwa tingkat pengembalian untuk "Mrs" hampir seimbang. Sapaan "Mr", 2.05% barang tidak dikembalikan dan 1.43% dikembalikan, proporsi barang yang tidak dikembalikan lebih tinggi daripada yang dikembalikan. Untuk "Company", "Family", dan "not reported", jumlah observasi sangat kecil, sehingga persentasenya juga kecil.

Terlihat bahwa dalam setiap kategori salutation, jumlah barang yang tidak dikembalikan cenderung lebih tinggi daripada jumlah barang yang dikembalikan, meskipun perbedaannya paling kecil pada "Mrs".

In [61]:
# Distribusi data kategorikal State
# Menghitung frekuensi kategori
state_counts = df2['state'].value_counts().reset_index()
state_counts.columns = ['Kategori', 'Jumlah']

# Membuat bar chart horizontal menggunakan Plotly Express
fig = px.bar(state_counts, x='Jumlah', y='Kategori', orientation='h', 
             title='Frekuensi State', labels={'Jumlah': 'Jumlah', 'Kategori': 'State'},
             color='Jumlah', color_continuous_scale='Earth')


fig.update_traces(texttemplate='%{x:d}', textposition='outside')
fig.update_layout(title_x=0.5)

# Menampilkan plot
fig.show()
In [62]:
# Menghitung frekuensi kategori
state_percentage = df2['state'].value_counts().reset_index()

# Menghitung persentase untuk setiap kategori
state_percentage.columns = ['Kategori', 'Jumlah']
state_percentage['Persentase'] = (state_percentage['Jumlah'] / state_percentage['Jumlah'].sum()) * 100

# Membuat bar chart horizontal menggunakan Plotly Express dengan persentase
fig = px.bar(state_percentage, x='Persentase', y='Kategori', orientation='h', 
             title='Persentase State', labels={'Persentase': 'Persentase', 'Kategori': 'State'},
             color='Persentase', color_continuous_scale='Earth')

fig.update_traces(texttemplate='%{x:.2f}%', textposition='outside')
fig.update_layout(
    showlegend=False,
    title_x=0.5
)

# Menampilkan plot
fig.show()

Negara bagian North Rhine-Westphalia memiliki persentase tertinggi, yaitu 23.1%, ini secara signifikan lebih tinggi dibandingkan negara bagian lainnya. Lower Saxony, Bavaria, dan Baden-Wuerttemberg, tiga negara bagian ini memiliki persentase yang cukup tinggi, berkisar antara 13% hingga 14%. Sebagian besar negara bagian lainnya memiliki persentase di bawah 7%, dan beberapa bahkan di bawah 1%.

In [63]:
#visualisasi hubungan salutation dengan returnShipment
plt.figure(figsize=(8, 8))
total = float(len(df2['state']))
ax = sns.countplot(y="state", hue="returnShipment", data=df2)

# Menambahkan anotasi persentase di atas setiap batang
for p in ax.patches:
    # Menghitung persentase
    percentage = 100 * p.get_width() / total
    # Menentukan posisi anotasi (di sebelah kanan batang)
    ax.annotate(
        f'{percentage:.2f}%', 
        (p.get_x() + p.get_width(), p.get_y() + p.get_height() / 2.),  # Posisi di kanan batang
        ha='left', va='center', 
        xytext=(6, 0), textcoords='offset points', fontsize=8
    )

plt.show()

Dari semua data negara bagain "North Rhine-Westphalia", 12% barang tidak dikembalikan dan 11% dikembalikan, perbedaan ini relatif kecil, menunjukkan bahwa tingkat pengembalian untuk negara bagain "North Rhine-Westphalia" hampir seimbang. Negara bagian lainnya juga terlihat untuk kategori barang tidak dikembalikan sedikit dibandingkan barang yang dikembalikan. Terlihat juga beberapa negara bagian dengan dengan persentase kecil dengan jumlah barang yang dikembalikan sedikit tinggi nol sekian persen daripada jumlah barang yang tidak dikembalikan.

Training Data¶

In [64]:
# Salin data df2
df_train = df2.copy()
In [65]:
# Encoding atribut targert 'returnShipment' menggunakan LabelEncoder
from sklearn import preprocessing
le = preprocessing.LabelEncoder()
le.fit(df_train.returnShipment)
Y = le.transform(df_train.returnShipment)
In [66]:
# One hot encoding untuk data bertipe categorical
df_train = pd.get_dummies(data=df_train, columns=['size', 'color', 'salutation', 'state'])
In [67]:
df_train.head()
Out[67]:
orderItemID orderDate deliveryDate itemID manufacturerID price customerID dateOfBirth creationDate returnShipment ... state_Hesse state_Lower Saxony state_Mecklenburg-Western Pomerania state_North Rhine-Westphalia state_Rhineland-Palatinate state_Saarland state_Saxony state_Saxony-Anhalt state_Schleswig-Holstein state_Thuringia
0 1 2012-04-01 2012-04-03 186 25 69.90 794 1965-01-06 2011-04-25 0 ... 0 0 0 0 0 0 0 0 0 0
1 2 2012-04-01 2012-04-03 71 21 69.95 794 1965-01-06 2011-04-25 1 ... 0 0 0 0 0 0 0 0 0 0
2 3 2012-04-01 2012-04-03 71 21 69.95 794 1965-01-06 2011-04-25 1 ... 0 0 0 0 0 0 0 0 0 0
3 4 2012-04-02 2012-04-07 22 14 39.90 808 1959-11-09 2012-01-04 0 ... 0 0 0 0 0 0 1 0 0 0
4 5 2012-04-02 2012-04-05 151 53 29.90 825 1964-07-11 2011-02-16 0 ... 0 0 0 0 1 0 0 0 0 0

5 rows × 63 columns

In [68]:
# Siapkan fitur untuk training dengan membuang kelas label (returnShipment)
X = df_train.drop("returnShipment", axis=1)
In [69]:
# Mengubah data bertipe datetime untuk keperluan klasifikasi
import datetime as dt
X['orderDate'] = X['orderDate'].map(dt.datetime.toordinal)
X['deliveryDate'] = X['deliveryDate'].map(dt.datetime.toordinal)
X['dateOfBirth'] = X['dateOfBirth'].map(dt.datetime.toordinal)
X['creationDate'] = X['creationDate'].map(dt.datetime.toordinal)
In [70]:
# Split Dataset, 80% sebagai data train dan 20% sisanya sebagai data test
from sklearn.model_selection import train_test_split

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42)
In [71]:
# Simpan nama kolom untuk keperluan prediksi nanti
import pickle
with open('train_v1.pickle', 'wb') as fp:
    pickle.dump(X_train.columns, fp)

Klasifikasi¶

In [72]:
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from sklearn.metrics import classification_report, make_scorer
from sklearn.model_selection import cross_val_score
from sklearn.metrics import confusion_matrix
In [73]:
# Klasifikasi dengan Naive Bayes
from sklearn.naive_bayes import GaussianNB

# Inisialisasi Naive Bayes
nb_clf = GaussianNB()

# Melatih model Naive Bayes dengan data latih
nb_clf.fit(X_train, Y_train)

# Menggunakan model Naive Bayes untuk melakukan prediksi pada data uji
nb_pred = nb_clf.predict(X_test)

# Menghitung masing-masing metrik evaluasi
nb_accuracy = accuracy_score(Y_test, nb_pred)
nb_precision = precision_score(Y_test, nb_pred)
nb_recall = recall_score(Y_test, nb_pred)
nb_f1 = f1_score(Y_test, nb_pred)

# Menampilkan masing-masing metrik evaluasi
print(f"Accuracy: {nb_accuracy:.3f}")
print(f"Precision: {nb_precision:.3f}")
print(f"Recall: {nb_recall:.3f}")
print(f"F1 Score: {nb_f1:.3f}")
print("\n")
print(classification_report(Y_test, nb_pred))
Accuracy: 0.556
Precision: 0.545
Recall: 0.462
F1 Score: 0.500


              precision    recall  f1-score   support

           0       0.56      0.64      0.60     49937
           1       0.55      0.46      0.50     46282

    accuracy                           0.56     96219
   macro avg       0.55      0.55      0.55     96219
weighted avg       0.55      0.56      0.55     96219

In [74]:
# Klasifikasi dengan Decision Tree
from sklearn import tree

# Inisialisasi Decision Tree
dt_clf = tree.DecisionTreeClassifier()

# Melatih model Decsion Tree dengan data latih
dt_clf.fit(X_train, Y_train)

# Menggunakan model Decision Tree untuk melakukan prediksi pada data uji
dt_pred = dt_clf.predict(X_test)

# Menghitung masing-masing metrik evaluasi
dt_accuracy = accuracy_score(Y_test, dt_pred)
dt_precision = precision_score(Y_test, dt_pred)
dt_recall = recall_score(Y_test, dt_pred)
dt_f1 = f1_score(Y_test, dt_pred)

# Menampilkan masing-masing metrik evaluasi
print(f"Accuracy: {dt_accuracy:.3f}")
print(f"Precision: {dt_precision:.3f}")
print(f"Recall: {dt_recall:.3f}")
print(f"F1 Score: {dt_f1:.3f}")
print("\n")
print(classification_report(Y_test, dt_pred))
Accuracy: 0.579
Precision: 0.560
Recall: 0.573
F1 Score: 0.567


              precision    recall  f1-score   support

           0       0.60      0.58      0.59     49937
           1       0.56      0.57      0.57     46282

    accuracy                           0.58     96219
   macro avg       0.58      0.58      0.58     96219
weighted avg       0.58      0.58      0.58     96219

In [75]:
# Klasifikasi dengan Random Forest
from sklearn.ensemble import RandomForestClassifier

# Inisialisasi RandomForestClassifier
rf_clf = RandomForestClassifier(max_depth = 10, 
                             n_estimators=100, 
                             random_state=42)

# Melatih model Random Forest dengan data latih
rf_clf.fit(X_train, Y_train)

# Menggunakan model Random Forest untuk melakukan prediksi pada data uji
rf_pred = rf_clf.predict(X_test)

# Menghitung masing-masing metrik evaluasi
rf_accuracy = accuracy_score(Y_test, rf_pred)
rf_precision = precision_score(Y_test, rf_pred)
rf_recall = recall_score(Y_test, rf_pred)
rf_f1 = f1_score(Y_test, rf_pred)

# Menampilkan masing-masing metrik evaluasi
print(f"Accuracy: {rf_accuracy:.3f}")
print(f"Precision: {rf_precision:.3f}")
print(f"Recall: {rf_recall:.3f}")
print(f"F1 Score: {rf_f1:.3f}")
print("\n")
print(classification_report(Y_test, rf_pred))
Accuracy: 0.591
Precision: 0.569
Recall: 0.619
F1 Score: 0.593


              precision    recall  f1-score   support

           0       0.62      0.57      0.59     49937
           1       0.57      0.62      0.59     46282

    accuracy                           0.59     96219
   macro avg       0.59      0.59      0.59     96219
weighted avg       0.59      0.59      0.59     96219

In [76]:
# Klasifikasi dengan XGBoost
import xgboost as xgb
from xgboost import XGBClassifier

# Inisialisasi XGBClassifier
xgb_clf = XGBClassifier(objective = 'binary:logistic', booster = 'gbtree',
                    learning_rate = 0.05, max_depth = 8,
                    subsample = 0.7, min_child_weight = 5,
                    gamma = 0.1, colsample_bytree = 0.7,
                    n_estimators=200, random_state=42)

# Melatih model XGBoost dengan data latih
xgb_clf.fit(X_train, Y_train)

# Menggunakan model XGBoost untuk melakukan prediksi pada data uji
xgb_pred = xgb_clf.predict(X_test)

# Menghitung masing-masing metrik evaluasi
xgb_accuracy = accuracy_score(Y_test, xgb_pred)
xgb_recall = recall_score(Y_test, xgb_pred)
xgb_precision = precision_score(Y_test, xgb_pred)
xgb_f1 = f1_score(Y_test, xgb_pred)

# Menampilkan masing-masing metrik evaluasi
print(f"Accuracy: {xgb_accuracy:.3f}")
print(f"Precision: {xgb_precision:.3f}")
print(f"Recall: {xgb_recall:.3f}")
print(f"F1 Score: {xgb_f1:.3f}")
print("\n")
print(classification_report(Y_test, xgb_pred))
Accuracy: 0.621
Precision: 0.600
Recall: 0.638
F1 Score: 0.618


              precision    recall  f1-score   support

           0       0.64      0.61      0.62     49937
           1       0.60      0.64      0.62     46282

    accuracy                           0.62     96219
   macro avg       0.62      0.62      0.62     96219
weighted avg       0.62      0.62      0.62     96219

Dalam kasus prediksi pengembalian barang atau tidak, Recall untuk memastikan semua pesanan yang mungkin akan dikembalikan bisa dideteksi dengan baik. Sehingga fokus pada minimisasi False Negatives untuk memastikan bahwa barang yang berpotensi dikembalikan teridentifikasi dengan baik. False Negatives (pesanan yang akan dikembalikan tetapi diprediksi tidak akan dikembalikan) berisiko tinggi karena akan menyebabkan biaya tambahan seperti stok tidak siap untuk dijual ulang, pelanggan kecewa karena pengembalian tidak diantisipasi.

Presisi untuk meminimalkan prediksi pengembalian yang salah atau untuk memastikan bahwa prediksi pengembalian barang benar-benar akurat. Dalam kasus pengembalian barang, baik precision dan recall penting, karena Recall diperlukan untuk menangkap semua pengembalian barang sedangkan Precision diperlukan untuk mengurangi kesalahan prediksi "barang akan dikembalikan" yang sebenarnya tidak akan dikembalikan. Untuk mencari keseimbangan antara recall dan presisi dapat menggunakan F1-score. F1-score memberikan keseimbangan antara precision dan recall.

Sehingga untuk model yang akan digunakan untuk prediksi dilihat dari hasil Recall dan F1-Score.

In [77]:
# Hasil evaluasi untuk berbagai model
model_evaluations = {
    "Naive Bayes": {"Accuracy": nb_accuracy, "Precision": nb_precision, "Recall": nb_recall, "F1 Score": nb_f1},
    "Decision Tree": {"Accuracy": dt_accuracy, "Precision": dt_precision, "Recall": dt_recall, "F1 Score": dt_f1},
    "Random Forest": {"Accuracy": rf_accuracy, "Precision": rf_precision, "Recall": rf_recall, "F1 Score": rf_f1},
    "XGBoost": {"Accuracy": xgb_accuracy, "Precision": xgb_precision, "Recall": xgb_recall, "F1 Score": xgb_f1}
}
In [78]:
# Menentukan model terbaik berdasarkan F1-score
best_model = max(model_evaluations, key=lambda x: model_evaluations[x]["Recall"])
print(f"Model terbaik adalah {best_model} dengan Recall {model_evaluations[best_model]['Recall']:.3f}.")
Model terbaik adalah XGBoost dengan Recall 0.638.
In [79]:
# Menentukan model terbaik berdasarkan F1-score
best_model = max(model_evaluations, key=lambda x: model_evaluations[x]["Recall"])
print(f"Model terbaik adalah {best_model} dengan F1-Score {model_evaluations[best_model]['F1 Score']:.3f}.")
Model terbaik adalah XGBoost dengan F1-Score 0.618.

Recall yang lebih tinggi menunjukkan bahwa model lebih baik dalam mengidentifikasi pengembalian barang. Dari hasil klasifikasi beberapa model di atas, model dengan nilai Recall dan F1-Score tertinggi adalah model XGBoost. Ini berarti bahwa XGBoost lebih baik dalam mendeteksi pengembalian barang dibandingkan model lainnya.

Cross Validation¶
In [80]:
# Cross Validation dengan Random Forest
def classification_report_with_score(y_true, y_pred):
    # Menampilkan classification report dan confusion matrix
    print(classification_report(y_true, y_pred, zero_division=0))
    print(confusion_matrix(y_true, y_pred))
    return accuracy_score(y_true, y_pred)

# Inisialisasi RandomForestClassifier
rf_clf = RandomForestClassifier(max_depth = 10, 
                             n_estimators=100, 
                             random_state=42)

# Melatih model Random Forest dengan data latih
rf_clf.fit(X_train, Y_train)

# Evaluasi dengan cross-validation
scores = cross_val_score(rf_clf, X=X, y=Y, cv=5,
                         scoring=make_scorer(classification_report_with_score))

# Menampilkan hasil cross-validation
print("\nSummary Cross Validatoin: ")
print(f"Cross-validation scores: {scores}")
print(f"Mean accuracy: {scores.mean():.3f}")
print(f"Standard deviation: {scores.std():.3f}")
              precision    recall  f1-score   support

           0       0.86      0.00      0.01     49801
           1       0.48      1.00      0.65     46418

    accuracy                           0.48     96219
   macro avg       0.67      0.50      0.33     96219
weighted avg       0.68      0.48      0.32     96219

[[  137 49664]
 [   22 46396]]
              precision    recall  f1-score   support

           0       0.52      1.00      0.68     49800
           1       0.00      0.00      0.00     46419

    accuracy                           0.52     96219
   macro avg       0.26      0.50      0.34     96219
weighted avg       0.27      0.52      0.35     96219

[[49800     0]
 [46419     0]]
              precision    recall  f1-score   support

           0       0.48      0.45      0.46     49800
           1       0.44      0.47      0.45     46418

    accuracy                           0.46     96218
   macro avg       0.46      0.46      0.46     96218
weighted avg       0.46      0.46      0.46     96218

[[22538 27262]
 [24791 21627]]
              precision    recall  f1-score   support

           0       0.44      0.48      0.46     49800
           1       0.39      0.35      0.37     46418

    accuracy                           0.42     96218
   macro avg       0.42      0.42      0.42     96218
weighted avg       0.42      0.42      0.42     96218

[[24096 25704]
 [30133 16285]]
              precision    recall  f1-score   support

           0       0.00      0.00      0.00     49800
           1       0.48      1.00      0.65     46418

    accuracy                           0.48     96218
   macro avg       0.24      0.50      0.33     96218
weighted avg       0.23      0.48      0.31     96218

[[    0 49800]
 [    0 46418]]

Summary Cross Validatoin: 
Cross-validation scores: [0.4836155  0.5175693  0.45900975 0.41968239 0.48242533]
Mean accuracy: 0.472
Standard deviation: 0.032
In [81]:
# Cross Validation model XGBoost
def classification_report_with_score(y_true, y_pred):
    # Menampilkan classification report dan confusion matrix
    print(classification_report(y_true, y_pred, zero_division=0)) 
    print(confusion_matrix(y_true, y_pred))
    return accuracy_score(y_true, y_pred)

# Inisialisasi XGBoost
xgb_clf = XGBClassifier(objective = 'binary:logistic', booster = 'gbtree',
                    learning_rate = 0.05, max_depth = 8,
                    subsample = 0.7, min_child_weight = 5,
                    gamma = 0.1, colsample_bytree = 0.7,
                    n_estimators=200, random_state=42)

# Melatih model XGBoost dengan data latih
xgb_clf.fit(X_train, Y_train)

# Evaluasi dengan cross-validation 
scores = cross_val_score(xgb_clf, X=X, y=Y, cv=5,
                         scoring=make_scorer(classification_report_with_score))


# Menampilkan hasil cross-validation
print("\nSummary Cross Validation: ")
print(f"Cross-validation scores: {scores}")
print(f"Mean accuracy: {scores.mean():.3f}")
print(f"Standard deviation: {scores.std():.3f}")
              precision    recall  f1-score   support

           0       0.68      0.22      0.33     49801
           1       0.51      0.89      0.65     46418

    accuracy                           0.54     96219
   macro avg       0.60      0.55      0.49     96219
weighted avg       0.60      0.54      0.49     96219

[[10842 38959]
 [ 5067 41351]]
              precision    recall  f1-score   support

           0       0.52      1.00      0.68     49800
           1       0.64      0.01      0.01     46419

    accuracy                           0.52     96219
   macro avg       0.58      0.50      0.35     96219
weighted avg       0.58      0.52      0.36     96219

[[49652   148]
 [46158   261]]
              precision    recall  f1-score   support

           0       0.39      0.16      0.23     49800
           1       0.45      0.72      0.55     46418

    accuracy                           0.43     96218
   macro avg       0.42      0.44      0.39     96218
weighted avg       0.42      0.43      0.39     96218

[[ 8166 41634]
 [12812 33606]]
              precision    recall  f1-score   support

           0       0.46      0.51      0.49     49800
           1       0.41      0.36      0.38     46418

    accuracy                           0.44     96218
   macro avg       0.44      0.44      0.44     96218
weighted avg       0.44      0.44      0.44     96218

[[25573 24227]
 [29585 16833]]
              precision    recall  f1-score   support

           0       0.00      0.00      0.00     49800
           1       0.48      1.00      0.65     46418

    accuracy                           0.48     96218
   macro avg       0.24      0.50      0.33     96218
weighted avg       0.23      0.48      0.31     96218

[[    0 49800]
 [    0 46418]]

Summary Cross Validation: 
Cross-validation scores: [0.54243964 0.5187437  0.43413914 0.44072835 0.48242533]
Mean accuracy: 0.484
Standard deviation: 0.042

Random Forest memiliki performa moderat dengan tingkat akurasi yang tidak terlalu tinggi. XGBoost memberikan performa yang lebih baik dibandingkan Random Forest. XGBoost memiliki akurasi rata-rata yang lebih tinggi (48.4%) dibandingkan Random Forest (47.2%). Ini menunjukkan bahwa XGBoost lebih efektif dalam menangkap pola dalam data.

In [82]:
# Confusion Matrix dengan XGBoost
plt.figure(figsize = (6, 4))
sns.heatmap(confusion_matrix(Y_test, xgb_pred), annot = True, fmt="d", cmap="Blues")
plt.xlabel("Predicted Label")
plt.ylabel("True Label")
plt.title("Confusion Matrix")
plt.show()
  • Sebanyak 30,233 data dengan label sebenarnya 0 diprediksi sebagai 0 oleh model (True Negatives), model berhasil memprediksi dengan benar bahwa barang tidak dikembalikan.
  • Sebanyak 19,704 data dengan label sebenarnya 0 diprediksi sebagai 1 oleh model (False Positives), model salah memprediksi bahwa barang akan dikembalikan padahal sebenarnya tidak.
  • Sebanyak 16,747 data dengan label sebenarnya 1 diprediksi sebagai 0 oleh model (False Negatives), model salah memprediksi bahwa barang tidak akan dikembalikan padahal sebenarnya dikembalikan.
  • Sebanyak 29,535 data dengan label sebenarnya 1 diprediksi sebagai 1 oleh model (True Positives), model berhasil memprediksi dengan benar bahwa barang dikembalikan.

Model memiliki performa yang lumayan untuk mendeteksi barang yang dikembalikan (recall 63.8%), yang penting untuk mengidentifikasi semua potensi pengembalian.

In [83]:
# Simpan model dengan joblib
import joblib as jb

jb.dump(xgb_clf, "model_xgb.joblib")
Out[83]:
['model_xgb.joblib']

Prediksi¶

In [84]:
df3 = pd.read_csv("E:\\data\\orders_class.txt", sep=';', na_values=["?"])
In [85]:
df3.head()
Out[85]:
orderItemID orderDate deliveryDate itemID size color manufacturerID price customerID salutation dateOfBirth state creationDate
0 1 2013-04-01 2013-04-03 2347 43 magenta 1 89.9 12489 Mrs 1963-04-26 Hesse 2012-04-23
1 2 2013-04-01 2013-04-03 2741 43 grey 1 99.9 12489 Mrs 1963-04-26 Hesse 2012-04-23
2 3 2013-04-01 2013-04-03 2514 9 ecru 19 79.9 12489 Mrs 1963-04-26 Hesse 2012-04-23
3 4 2013-04-01 2013-05-06 2347 42 brown 1 89.9 12489 Mrs 1963-04-26 Hesse 2012-04-23
4 5 2013-04-01 NaN 2690 43 grey 1 119.9 12489 Mrs 1963-04-26 Hesse 2012-04-23
In [86]:
df3.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50078 entries, 0 to 50077
Data columns (total 13 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   orderItemID     50078 non-null  int64  
 1   orderDate       50078 non-null  object 
 2   deliveryDate    45810 non-null  object 
 3   itemID          50078 non-null  int64  
 4   size            50078 non-null  object 
 5   color           50078 non-null  object 
 6   manufacturerID  50078 non-null  int64  
 7   price           50078 non-null  float64
 8   customerID      50078 non-null  int64  
 9   salutation      50078 non-null  object 
 10  dateOfBirth     44909 non-null  object 
 11  state           50078 non-null  object 
 12  creationDate    50078 non-null  object 
dtypes: float64(1), int64(4), object(8)
memory usage: 5.0+ MB
In [87]:
# Mengubah semua tipe data fitur dengan format tanggal menjadi datetime
kategori_waktu = ['orderDate', 'deliveryDate', 'dateOfBirth', 'creationDate']
#Looping untuk merubah type data'
for column in kategori_waktu:
    df3[column] = df3[column].astype('datetime64[ns]')
In [88]:
df3.dtypes
Out[88]:
orderItemID                int64
orderDate         datetime64[ns]
deliveryDate      datetime64[ns]
itemID                     int64
size                      object
color                     object
manufacturerID             int64
price                    float64
customerID                 int64
salutation                object
dateOfBirth       datetime64[ns]
state                     object
creationDate      datetime64[ns]
dtype: object
In [89]:
# Mengubah semua tipe data fitur objek menjadi kategori
kategori_objek = ['size', 'color', 'salutation', 'state']
#Looping untuk merubah type data'
for column in kategori_objek:
    df3[column] = df3[column].astype('category')
In [90]:
df3.dtypes
Out[90]:
orderItemID                int64
orderDate         datetime64[ns]
deliveryDate      datetime64[ns]
itemID                     int64
size                    category
color                   category
manufacturerID             int64
price                    float64
customerID                 int64
salutation              category
dateOfBirth       datetime64[ns]
state                   category
creationDate      datetime64[ns]
dtype: object
In [91]:
df3.isnull().sum()
Out[91]:
orderItemID          0
orderDate            0
deliveryDate      4268
itemID               0
size                 0
color                0
manufacturerID       0
price                0
customerID           0
salutation           0
dateOfBirth       5169
state                0
creationDate         0
dtype: int64

Data kosong pada data yang akan diprediksi yaitu pada fitur deliveryDate dan dateOfBirth. Proses menangani data kosong ini sama seperti pada proses untuk data training

In [92]:
# Melihat nilai unik dari DeliveryDate
df3['deliveryDate'].dt.year.unique()
Out[92]:
array([2013.,   nan, 1990.])
In [93]:
df3[df3['deliveryDate'].dt.year == 1990][['orderDate', 'deliveryDate']].head(10)
Out[93]:
orderDate deliveryDate
562 2013-04-01 1990-12-31
973 2013-04-01 1990-12-31
1278 2013-04-01 1990-12-31
1279 2013-04-01 1990-12-31
1512 2013-04-01 1990-12-31
1607 2013-04-01 1990-12-31
1643 2013-04-02 1990-12-31
1828 2013-04-02 1990-12-31
1829 2013-04-02 1990-12-31
1830 2013-04-02 1990-12-31
In [94]:
import random

# Fungsi untuk mengganti tahun, bulan, dan hari deliveryDate mengacu pada orderDate
def adjust_delivery_date(row):
    # Memeriksa apakah tahun deliveryDate adalah 1990
    if row['deliveryDate'].year == 1990:
        # Ambil bulan dan tahun dari orderDate
        year = row['orderDate'].year
        month = row['orderDate'].month
        
        # Ambil + 3-5 hari dari orderDate 
        day = row['orderDate'].day + random.choice([3, 5])
        
        # Menghindari kesalahan jika hari yang ditambahkan melebihi batas bulan
        try:
            adjusted_date = row['deliveryDate'].replace(year=year, month=month, day=day)
        except ValueError:
            # Jika hari yang ditambahkan melebihi batas, set ke hari terakhir bulan tersebut
            adjusted_date = row['deliveryDate'].replace(year=year, month=month, day=1) + pd.Timedelta(days=-1)
        
        return adjusted_date
    return row['deliveryDate']

# Terapkan fungsi dengan modus
df3['deliveryDate'] = df3.apply(adjust_delivery_date, axis=1)
In [95]:
# Melihat nilai unik dari DeliveryDate
df3['deliveryDate'].dt.year.unique()
Out[95]:
array([2013.,   nan])
In [96]:
# Fungsi untuk mengisi deliveryDate berdasarkan orderDate
def fill_delivery_date(row):
    if pd.isna(row['deliveryDate']):  # Jika deliveryDate kosong
        # Tambahkan antara 2 hingga 5 hari ke orderDate
        penambahan_hari = random.randint(2, 5)
        return row['orderDate'] + pd.Timedelta(days=penambahan_hari)
    return row['deliveryDate']  # Jika tidak kosong, biarkan deliveryDate tetap

# Terapkan fungsi ke dataframe
df3['deliveryDate'] = df3.apply(fill_delivery_date, axis=1)
In [97]:
# Melihat nilai unik dari DeliveryDate
df3['deliveryDate'].dt.year.unique()
Out[97]:
array([2013], dtype=int64)
In [98]:
df3.isnull().sum()
Out[98]:
orderItemID          0
orderDate            0
deliveryDate         0
itemID               0
size                 0
color                0
manufacturerID       0
price                0
customerID           0
salutation           0
dateOfBirth       5169
state                0
creationDate         0
dtype: int64
In [99]:
# Melihat nilai maksimal dan minimal dateOfBirth
print("Tanggal Max dateOfBirth :", df3.dateOfBirth.max())
print("Tanggal Min dateOfBirth :", df3.dateOfBirth.min())
Tanggal Max dateOfBirth : 2012-11-18 00:00:00
Tanggal Min dateOfBirth : 1900-11-19 00:00:00
In [100]:
# Melihat nilai unik dari dateOfBirth
df3['dateOfBirth'].dt.year.unique()
Out[100]:
array([1963., 1956., 1967., 1958.,   nan, 1986., 1957., 1953., 1952.,
       1962., 1971., 1976., 1975., 1950., 1973., 1960., 1968., 1979.,
       1951., 1955., 1970., 1959., 1946., 1972., 1949., 1969., 1974.,
       1965., 1961., 1966., 1944., 1981., 1964., 1983., 1978., 1954.,
       1977., 1945., 1985., 1900., 1947., 1982., 1987., 1948., 1993.,
       1984., 1990., 1992., 1936., 1980., 1938., 1988., 1901., 1937.,
       1942., 2011., 1994., 1943., 1999., 1941., 1940., 1991., 1939.,
       1931., 1935., 1933., 2010., 1989., 1925., 1934., 2012., 1929.,
       1928., 2009., 1998., 1930., 1919.])
In [101]:
# Hitung median tahun yang valid (1935-1997)
tahun_valid = df3[df3['dateOfBirth'].dt.year.between(1935, 1997)]['dateOfBirth'].dt.year
median_tahun = tahun_valid.median()

# Ganti tahun yang tidak valid (di bawah 1935 atau di atas 1997) dengan median
df3['dateOfBirth'] = df3['dateOfBirth'].apply(
    lambda x: pd.Timestamp(f"{int(median_tahun)}-{x.month:02d}-{x.day:02d}")
    if x.year < 1935 or x.year > 1997 else x
)
In [102]:
# Melihat nilai maksimal dan minimal dateOfBirth setelah mengubah dengan median
print("Tanggal Max dateOfBirth :", df3.dateOfBirth.max())
print("Tanggal Min dateOfBirth :", df3.dateOfBirth.min())
Tanggal Max dateOfBirth : 1994-06-20 00:00:00
Tanggal Min dateOfBirth : 1935-03-09 00:00:00
In [103]:
# Melihat nilai unik dari dateOfBirth
df3['dateOfBirth'].dt.year.unique()
Out[103]:
array([1963., 1956., 1967., 1958.,   nan, 1986., 1957., 1953., 1952.,
       1962., 1971., 1976., 1975., 1950., 1973., 1960., 1968., 1979.,
       1951., 1955., 1970., 1959., 1946., 1972., 1949., 1969., 1974.,
       1965., 1961., 1966., 1944., 1981., 1964., 1983., 1978., 1954.,
       1977., 1945., 1985., 1947., 1982., 1987., 1948., 1993., 1984.,
       1990., 1992., 1936., 1980., 1938., 1988., 1937., 1942., 1994.,
       1943., 1941., 1940., 1991., 1939., 1935., 1989.])
In [104]:
# Fungsi untuk mengganti tahun yang hilang dengan median
def replace_with_median(row):
    if pd.isna(row):  # Jika data kosong (NaN) atau NaT
        # Jika data kosong, ganti tahun dengan median
        return pd.Timestamp(f"{int(median_tahun)}-01-01")
    # Jika data tidak kosong, kembalikan baris tanpa perubahan
    return row

# Terapkan fungsi ke dataframe
df3['dateOfBirth'] = df3['dateOfBirth'].apply(replace_with_median)
In [105]:
df3.isnull().sum()
Out[105]:
orderItemID       0
orderDate         0
deliveryDate      0
itemID            0
size              0
color             0
manufacturerID    0
price             0
customerID        0
salutation        0
dateOfBirth       0
state             0
creationDate      0
dtype: int64
In [106]:
# Cek hasil fitur size
df3['size'].unique()
Out[106]:
['43', '9', '42', '41', '7+', ..., '3634', '14', '3834', '56', '46+']
Length: 100
Categories (100, object): ['1', '10', '10+', '104', ..., 'unsized', 'xl', 'xxl', 'xxxl']
In [107]:
# Normalisasi dan buang tanda '+'
df3['size'] = df3['size'].str.upper()  # Ubah ke kapital semua
df3['size'] = df3['size'].str.replace(r'\+', '', regex=True)  # Hapus tanda '+'

# Mengelompokkan ukuran berdasarkan angka
def categorize_size(size):
    # Kategori ukuran standar
    size_categories = ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL']
    if size in size_categories:  # Jika sudah dalam kategori standar
        return size
    try:
        size_num = int(size)
        
        # Kategorisasi berdasarkan ukuran angka
        if size_num <= 10:
            return 'XS'
        elif 11 <= size_num <= 35:
            return 'S'
        elif 36 <= size_num <= 42:
            return 'M'
        elif 43 <= size_num <= 50:
            return 'L'
        elif 51 <= size_num <= 200:
            return 'XL'
        elif 201 <= size_num <= 1000:
            return 'XXL'
        elif size_num > 1000:
            return 'XXXL'
        
    except ValueError:
        return 'UNSIZED'


# Terapkan fungsi ke dataframe untuk mengkategorikan ukuran
df3['size'] = df3['size'].apply(categorize_size)
In [108]:
# Cek hasil fitur size
df3['size'].unique()
Out[108]:
array(['L', 'XS', 'M', 'S', 'UNSIZED', 'XL', 'XXL', 'XXXL'], dtype=object)
In [109]:
# Distribusi data kategorikal Size

# Menghitung frekuensi kategori
size_counts = df3['size'].value_counts().reset_index()
size_counts.columns = ['Kategori', 'Jumlah']

# Membuat bar chart horizontal menggunakan Plotly Express
fig = px.bar(size_counts, x='Jumlah', y='Kategori', orientation='h', 
             title='Frekuensi Fitur Size', labels={'Jumlah': 'Jumlah', 'Kategori': 'Size'},
             color='Jumlah', color_continuous_scale='Earth')

fig.update_layout(
    title_x=0.5,
)

# Menampilkan plot
fig.show()
In [110]:
# Cek nilai unik color
df3.color.unique()
Out[110]:
['magenta', 'grey', 'ecru', 'brown', 'blue', ..., 'leopard', 'almond', 'gold', 'lemon', 'antique pink']
Length: 66
Categories (66, object): ['almond', 'ancient', 'anthracite', 'antique pink', ..., 'terracotta', 'turquoise', 'white', 'yellow']
In [111]:
# Hitung frekuensi masing-masing kategori
color_counts = df3['color'].value_counts()

# Tentukan threshold untuk kategori yang jarang muncul
threshold = 1000  # Kemunculan kurang dari 1000 kali

# Ganti color yang jarang muncul dengan 'other'
df3['color'] = df3['color'].apply(lambda x: x if color_counts[x] >= threshold else 'other')
In [112]:
# Cek nilai unik color
df3.color.unique()
Out[112]:
array(['other', 'grey', 'ecru', 'brown', 'blue', 'white', 'purple',
       'green', 'black', 'stained', 'red', 'ocher', 'pink', 'olive',
       'denim', 'aquamarine', 'petrol'], dtype=object)
In [113]:
df3['color'].value_counts().head()
Out[113]:
black    8346
other    6853
green    5226
blue     4794
grey     4443
Name: color, dtype: int64
In [114]:
df_pred = df3.copy()
In [115]:
df_pred.dtypes
Out[115]:
orderItemID                int64
orderDate         datetime64[ns]
deliveryDate      datetime64[ns]
itemID                     int64
size                      object
color                     object
manufacturerID             int64
price                    float64
customerID                 int64
salutation              category
dateOfBirth       datetime64[ns]
state                   category
creationDate      datetime64[ns]
dtype: object
In [116]:
df_pred["size"] = df_pred["size"].astype("category")
df_pred["color"] = df_pred["color"].astype("category")
In [117]:
df_pred.dtypes
Out[117]:
orderItemID                int64
orderDate         datetime64[ns]
deliveryDate      datetime64[ns]
itemID                     int64
size                    category
color                   category
manufacturerID             int64
price                    float64
customerID                 int64
salutation              category
dateOfBirth       datetime64[ns]
state                   category
creationDate      datetime64[ns]
dtype: object
In [118]:
# Menyesuaikan tipe data kategori
from pandas.api.types import CategoricalDtype

kolom_kategori = ['size', 'color', 'salutation', 'state']

for kolom in kolom_kategori:
    data_kategori = CategoricalDtype(categories = df2[kolom].cat.categories, ordered=True)
    df_pred[kolom] = df_pred[kolom].astype(data_kategori)
In [119]:
# Mengaplikasikan one hot encoding untuk data bertipe categorical pada data prediksi
df_pred = pd.get_dummies(data=df_pred, columns=['size', 'color', 'salutation', 'state'])
In [120]:
# Mengubah fitur tipe data datetime menjadi ordinal untuk memudahkan model machine learning memproses
import datetime as dt

df_pred['orderDate'] = df_pred['orderDate'].map(dt.datetime.toordinal)
df_pred['deliveryDate'] = df_pred['deliveryDate'].map(dt.datetime.toordinal)
df_pred['dateOfBirth'] = df_pred['dateOfBirth'].map(dt.datetime.toordinal)
df_pred['creationDate'] = df_pred['creationDate'].map(dt.datetime.toordinal)
In [121]:
with open ('train_v1.pickle', 'rb') as fp:
    X_train_column = list(pickle.load(fp))

df_pred = df_pred[X_train_column]
In [122]:
#Prediksi dengan model yang sudah disimpan
clf = jb.load("model_xgb.joblib")
result = clf.predict(df_pred)
In [123]:
print(result[:100])
[1 1 1 1 1 0 1 1 1 1 0 1 0 0 1 0 0 0 1 0 1 1 1 1 0 1 1 1 1 0 0 1 0 0 0 0 0
 0 1 0 1 1 1 1 0 1 0 1 0 0 0 1 1 1 0 1 1 0 1 1 0 1 0 1 1 1 1 1 1 0 1 1 0 0
 0 0 0 1 1 1 1 1 0 0 1 0 1 0 0 0 1 1 0 1 1 1 1 1 1 0]
In [124]:
#Hasil prediksi
shipment_predict = len(result)
save_count = len(result[result == 0]) #barang disimpan
return_count = len(result[result == 1]) #barang dikembalikan
save_percentage = round(save_count/shipment_predict*100, 2)
return_percentage = round(return_count/shipment_predict*100, 2)

print('Hasil Prediksi:')
print('Total shipment {}'.format(shipment_predict))
print('Barang tidak dikembalikan {}'.format(save_count))
print('Persentase tidak dikembalikan {}%'.format(save_percentage))
print('Barang dikembalikan {}'.format(return_count))
print('Persentase dikembalikan {}%'.format(return_percentage))
Hasil Prediksi:
Total shipment 50078
Barang tidak dikembalikan 16711
Persentase tidak dikembalikan 33.37%
Barang dikembalikan 33367
Persentase dikembalikan 66.63%
In [125]:
#Visualisasi jumlah data prediksi yang tidak mengembalikan dan mengembalikan
plt.figure(figsize=(6, 6))
ax = sns.countplot(x=result, data = df_pred)
plt.title('Pediksi Return Shipment')
plt.xlabel('Return Shipment')
plt.ylabel('Count Return Shipment')

for p in ax.patches:
        ax.annotate(format(p.get_height(), 'd'), (p.get_x() + p.get_width() / 2., p.get_height()),
                    ha = 'center', va = 'center', xytext = (0, 6), textcoords = 'offset points', fontsize=8)
In [126]:
#Visualisasi persentase data prediksi yang tidak mengembalikan dan mengembalikan
plt.figure(figsize=(6, 6))
total = float(len(df_pred))
ax = sns.countplot(x=result, data=df_pred)
plt.title('Persentase Prediksi Return Shipment', fontsize=12)

for p in ax.patches:
        ax.annotate(format(100 * p.get_height()/total, '.2f') + '%', (p.get_x() + p.get_width() / 2., p.get_height()),
                    ha = 'center', va = 'center', xytext = (0, 6), textcoords = 'offset points', fontsize=8)
In [137]:
# Jika panjang berbeda
if len(Y_test) != len(result):
    min_length = min(len(Y_test), len(result))
    Y_test = Y_test[:min_length]
    result = result[:min_length]

accuracy = accuracy_score(Y_test, result)
precision = precision_score(Y_test, result)
recall = recall_score(Y_test, result)
f1 = f1_score(Y_test, result)

print("Accuracy:", accuracy)
print("Precision:", precision)
print("Recall:", recall)
print("F1-score:", f1)

# Menampilkan confusion matrix
conf_matrix = confusion_matrix(Y_test, result)
print("\nConfusion Matrix:\n", conf_matrix)

# Menampilkan laporan klasifikasi
print("\nClassification Report:\n", classification_report(Y_test, result))
Accuracy: 0.4921921801988897
Precision: 0.48179338867743576
Recall: 0.6638860210613257
F1-score: 0.5583689347365496

Confusion Matrix:
 [[ 8572 17291]
 [ 8139 16076]]

Classification Report:
               precision    recall  f1-score   support

           0       0.51      0.33      0.40     25863
           1       0.48      0.66      0.56     24215

    accuracy                           0.49     50078
   macro avg       0.50      0.50      0.48     50078
weighted avg       0.50      0.49      0.48     50078

In [141]:
overall_recall = recall_score(Y_test, result, average=None) 
print(f"Recall untuk masing-masing label: {overall_recall}")

# Recall untuk label 1
print(f"Recall untuk Label 1: {overall_recall[1]:.3f}")
Recall untuk masing-masing label: [0.33143873 0.66388602]
Recall untuk Label 1: 0.664

Hasil Prediksi:

  • Terlihat bahwa 66.63% dari barang diprediksi akan dikembalikan (label 1), sedangkan 33.37% diprediksi tidak dikembalikan (label 0). Mayoritas barang diprediksi akan dikembalikan (label 1) dibandingkan barang yang tidak dikembalikan (label 0), dengan selisih persentase sebesar 33.26%.
  • Grafik menunjukkan perbedaan yang cukup signifikan antara jumlah prediksi untuk kedua kategori, yang mana prediksi barang yang dikembalikan lebih tinggi daripada yang tidak dikembalikan.

Hasil Recall:

  • Recall untuk Label 0 (0.331), model hanya berhasil memprediksi dengan benar sekitar 33.1% dari total data yang seharusnya termasuk dalam kategori Label 0.
  • Recall untuk Label 1 (0.664), model berhasil memprediksi dengan benar sekitar 66.4% dari total data yang seharusnya termasuk dalam kategori Label 1. Ini lebih baik dibandingkan Label 0, menunjukkan model lebih fokus pada pengenalan kasus positif (return shipment).

Recall untuk label 1 cukup tinggi, karena fokus utama mengidentifikasi barang yang akan dikembalikan maka model ini cukup baik.

In [ ]: